Skip to content

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Feb 6, 2026

Summary

  • Fix structural JSDoc bugs: misplaced doc blocks in CasService (_chunkAndStore docs on _storeChunk, orphaned restore docs above _readAndVerifyChunks), broken @param import path in index.js
  • Add @param/@returns/@throws/@override/@abstract tags to every public and private method across all 17 source files
  • Add new documentation (GUIDE.md, docs/API.md, docs/SECURITY.md), examples, event tests, and benchmark updates

Test plan

  • ESLint passes with no warnings
  • All 328 unit tests pass
  • Pre-push hooks (lint + test) pass

Summary by CodeRabbit

Release Notes

  • New Features

    • File-oriented storage and restoration operations for simplified workflows
    • EventEmitter integration enabling progress tracking and lifecycle event monitoring
    • Tree creation and integrity verification capabilities
    • Encryption support via AES-256-GCM across all cryptographic adapters
    • Cross-platform crypto adapter (Web Crypto) for broader environment compatibility
  • Documentation

    • Comprehensive API reference guide
    • Security model and threat documentation
    • End-to-end user guide with core concepts
    • Three runnable example scripts demonstrating common workflows
  • Tests

    • Benchmark suite for performance validation
    • EventEmitter integration test coverage (14+ new tests)

Fix misplaced JSDoc blocks in CasService (orphaned restore docs,
_chunkAndStore docs attached to _storeChunk), correct broken import
path in index.js, and add @param/@returns/@throws/@override/@abstract
tags to every public and private method across the codebase.
@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Warning

Rate limit exceeded

@flyingrobots has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 36 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

PR introduces comprehensive documentation (guides, API references, security model), three runnable example scripts, EventEmitter integration into CasService with lifecycle event emissions, expanded public API (storeFile, restoreFile, createTree, verifyIntegrity), WebCryptoAdapter implementation, enhanced encryption/decryption workflows, and extensive benchmarking and event testing suites.

Changes

Cohort / File(s) Summary
Documentation & Roadmap
CHANGELOG.md, GUIDE.md, ROADMAP.md, docs/API.md, docs/SECURITY.md, examples/README.md
Added comprehensive documentation including end-to-end guide, API reference, security model, and examples documentation; marked M5 and M6 milestones as complete in roadmap.
Example Scripts
examples/store-and-restore.js, examples/encrypted-workflow.js, examples/progress-tracking.js
New runnable examples demonstrating basic store/restore workflow, encrypted workflows with key handling and integrity verification, and comprehensive progress event tracking with performance metrics.
CasService & Events
src/domain/services/CasService.js, test/unit/domain/services/CasService.events.test.js
Extended CasService as EventEmitter emitting lifecycle events (chunk:stored, file:stored, chunk:restored, file:restored, integrity:pass/fail, error); refactored chunk processing with dedicated _storeChunk and _readAndVerifyChunks helpers; enhanced key validation and encryption/decryption error handling; added comprehensive event testing suite with 220+ lines covering multi-chunk scenarios and failure modes.
Domain Models & Schemas
src/domain/errors/CasError.js, src/domain/schemas/ManifestSchema.js, src/domain/value-objects/Chunk.js, src/domain/value-objects/Manifest.js
Enhanced CasError with code and meta properties for machine-readable diagnostics; introduced ChunkSchema and EncryptionSchema; expanded ManifestSchema with size, chunks array, and optional encryption metadata; added toJSON() method to Manifest; improved error mapping and immutability enforcement in value objects.
Cryptographic Adapters
src/infrastructure/adapters/BunCryptoAdapter.js, src/infrastructure/adapters/NodeCryptoAdapter.js, src/infrastructure/adapters/WebCryptoAdapter.js
Added full AES-256-GCM encryption/decryption support across adapters; introduced createEncryptionStream for streaming encryption; added 32-byte key validation with CasError codes (INVALID_KEY_TYPE, INVALID_KEY_LENGTH); new WebCryptoAdapter for cross-platform support (Deno, browsers) with base64 metadata encoding; strengthened metadata construction (nonce, tag, algorithm).
Persistence & Ports
src/infrastructure/adapters/GitPersistenceAdapter.js, src/ports/GitPersistencePort.js, src/ports/CryptoPort.js
Added DEFAULT_POLICY with 30-second timeout and exponential backoff to GitPersistenceAdapter; updated JSDoc across port interfaces marking them as @abstract with clarified descriptions; enhanced method documentation for writeBlob, writeTree, readBlob, readTree.
Codecs & Public API
src/infrastructure/codecs/JsonCodec.js, src/infrastructure/codecs/CborCodec.js, src/ports/CodecPort.js, index.js
Added @override annotations to codec methods; marked CodecPort as @abstract; expanded ContentAddressableStore facade with new public methods (storeFile, restoreFile, createTree, verifyIntegrity), added chunkSize getter, implemented lazy service initialization with #getService/#initService pattern, and enhanced documentation for all public operations.
Tests & Benchmarks
test/benchmark/cas.bench.js
Expanded benchmark suite from minimal mocks to feature-rich harness with 200+ new lines covering plaintext/encrypted store/restore, multi-chunk createTree (10/100/1000 chunks), integrity verification, buffer encryption (1KB/1MB/10MB), and dual-codec performance (JSON/CBOR); integrated NodeCryptoAdapter and Manifest validation.
Configuration
eslint.config.js
Added ignores array to exclude examples/ directory from linting.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CAS as ContentAddressableStore<br/>(Facade)
    participant Service as CasService<br/>(EventEmitter)
    participant Crypto as CryptoAdapter
    participant Persistence as GitPersistence
    
    rect rgba(100, 150, 200, 0.5)
    Note over User,Persistence: Store & Progress Tracking
    User->>CAS: storeFile({filePath, slug, encryptionKey})
    CAS->>Service: store(source, slug, encryptionKey)
    
    loop For each chunk
        Service->>Service: _storeChunk(chunk, index)
        Service->>Crypto: encryptBuffer(chunk, key)
        Crypto-->>Service: {buf, meta}
        Service->>Persistence: writeBlob(encrypted)
        Persistence-->>Service: blobOid
        Service->>Service: emit('chunk:stored', {index, digest, ...})
    end
    
    Service->>Service: emit('file:stored', {slug, size, chunkCount, encrypted})
    Service-->>CAS: Manifest
    CAS-->>User: manifest
    end
    
    rect rgba(200, 150, 100, 0.5)
    Note over User,Persistence: Restore & Verification
    User->>CAS: restoreFile({manifest, encryptionKey, outputPath})
    CAS->>Service: restore(manifest, encryptionKey)
    
    loop For each chunk
        Service->>Persistence: readBlob(blobOid)
        Persistence-->>Service: encrypted chunk
        Service->>Service: _readAndVerifyChunks (verify digest)
        Service->>Service: emit('chunk:restored', {index, ...})
        Service->>Crypto: decryptBuffer(chunk, key, meta)
        Crypto-->>Service: decrypted chunk
    end
    
    Service->>Service: emit('file:restored', {slug, size, chunkCount})
    Service-->>CAS: buffer
    CAS->>CAS: write to outputPath
    CAS-->>User: {bytesWritten}
    end
    
    rect rgba(150, 200, 100, 0.5)
    Note over Service: Integrity Verification
    User->>CAS: verifyIntegrity(manifest)
    CAS->>Service: verifyIntegrity(manifest)
    
    alt All chunks match digests
        Service->>Service: emit('integrity:pass', {slug})
        Service-->>CAS: {valid: true}
    else Digest mismatch
        Service->>Service: emit('integrity:fail', {slug, chunkIndex, expected, actual})
        Service-->>CAS: {valid: false}
    end
    CAS-->>User: result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Whiskers twitching with glee
EventEmitters now sing so free,
Chunks hop and skip, encryption's tight,
A comprehensive guide shines bright.
From docs to examples, examples to tests—
This refactoring truly passes all quests! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main focus of this PR—a comprehensive JSDoc documentation pass across all source files, which is the primary change evident across the 17 source files modified.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cartographer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Combine M4 Compass changelog/roadmap entries with M5 Sonar + M6
Cartographer. Add JSDoc to new facade methods (readManifest,
deleteAsset, findOrphanedChunks).
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/domain/errors/CasError.js (1)

18-18: ⚠️ Potential issue | 🔴 Critical

Error.captureStackTrace is V8-specific and will throw in non-V8 runtimes.

The project supports Deno and Web environments (via WebCryptoAdapter), where Error.captureStackTrace may not exist. This line is called unconditionally and will throw a TypeError at runtime in those environments.

Proposed fix
-    Error.captureStackTrace(this, this.constructor);
+    if (typeof Error.captureStackTrace === 'function') {
+      Error.captureStackTrace(this, this.constructor);
+    }
src/infrastructure/adapters/NodeCryptoAdapter.js (1)

33-39: ⚠️ Potential issue | 🟡 Minor

decryptBuffer skips #validateKey — inconsistent with encryptBuffer.

encryptBuffer (line 21) calls this.#validateKey(key), but decryptBuffer does not. Passing an invalid key here will surface a raw node:crypto error instead of a descriptive CasError with INVALID_KEY_TYPE / INVALID_KEY_LENGTH. This is pre-existing behavior, but since this PR is the comprehensive JSDoc pass touching these methods, it's a good time to address the gap.

🛡️ Proposed fix
   /** `@override` */
   decryptBuffer(buffer, key, meta) {
+    this.#validateKey(key);
     const nonce = Buffer.from(meta.nonce, 'base64');
src/domain/value-objects/Manifest.js (1)

22-37: ⚠️ Potential issue | 🟠 Major

Discarded ManifestSchema.parse() result causes the encrypted default to be silently lost.

ManifestSchema.parse(data) is called for validation only — the return value (which carries Zod defaults and transforms) is discarded. This matters because EncryptionSchema defines encrypted: z.boolean().default(true). If a caller passes encryption metadata without the encrypted field, the schema parse succeeds (applying the default), but { ...data.encryption } on line 29 copies the raw input, which lacks encrypted.

🐛 Proposed fix — use the parsed result
     try {
-      ManifestSchema.parse(data);
-      this.slug = data.slug;
-      this.filename = data.filename;
-      this.size = data.size;
-      this.chunks = data.chunks.map((c) => new Chunk(c));
-      this.encryption = data.encryption ? { ...data.encryption } : undefined;
+      const validated = ManifestSchema.parse(data);
+      this.slug = validated.slug;
+      this.filename = validated.filename;
+      this.size = validated.size;
+      this.chunks = validated.chunks.map((c) => new Chunk(c));
+      this.encryption = validated.encryption ? { ...validated.encryption } : undefined;
       Object.freeze(this);
src/infrastructure/adapters/WebCryptoAdapter.js (1)

109-113: ⚠️ Potential issue | 🟠 Major

finalize() will crash with a null tag if called before the encrypt generator completes.

finalTag is initialized to null (line 83) and only set after the one-shot encrypt completes (line 104). If finalize() is called prematurely, this.#buildMeta(nonce, null)this.#toBase64(null) throws. BunCryptoAdapter guards against this with a streamFinalized flag — add the same guard here.

🛡️ Proposed fix
     const chunks = [];
     let finalTag = null;
+    let streamFinalized = false;

     const encrypt = async function* (source) {
       // ...
       finalTag = fullBuffer.slice(-tagLength);
+      streamFinalized = true;

       yield Buffer.from(ciphertext);
     };

     const finalize = () => {
+      if (!streamFinalized) {
+        throw new CasError(
+          'Cannot finalize before the encrypt stream is fully consumed',
+          'STREAM_NOT_CONSUMED',
+        );
+      }
       return this.#buildMeta(nonce, finalTag);
     };
index.js (1)

213-221: ⚠️ Potential issue | 🟠 Major

writeFileSync blocks the event loop — use writeFile (async) for a library API.

restoreFile is an async method, so callers expect it to be non-blocking. Using writeFileSync for potentially large restored files (10 MB+ per your benchmarks) will stall the event loop. Switch to the async fs.promises.writeFile.

Proposed fix
-import { createReadStream, writeFileSync } from 'node:fs';
+import { createReadStream } from 'node:fs';
+import { writeFile } from 'node:fs/promises';
 import path from 'node:path';
   async restoreFile({ manifest, encryptionKey, outputPath }) {
     const service = await this.#getService();
     const { buffer, bytesWritten } = await service.restore({
       manifest,
       encryptionKey,
     });
-    writeFileSync(outputPath, buffer);
+    await writeFile(outputPath, buffer);
     return { bytesWritten };
   }
src/domain/services/CasService.js (1)

273-275: ⚠️ Potential issue | 🟡 Minor

restore skips the file:restored event for empty manifests.

When manifest.chunks.length === 0, the method returns early (line 274) without emitting file:restored. Consumers listening for this event to track all restore operations will miss empty-file restores, breaking the documented contract that file:restored fires once per restore.

Proposed fix
     if (manifest.chunks.length === 0) {
+      this.emit('file:restored', {
+        slug: manifest.slug, size: 0, chunkCount: 0,
+      });
       return { buffer: Buffer.alloc(0), bytesWritten: 0 };
     }
🤖 Fix all issues with AI agents
In `@docs/SECURITY.md`:
- Line 63: Update the SECURITY.md line that claims the algorithm is "via Node.js
`node:crypto` module" to reflect multi-runtime support: state that the
underlying algorithm is AES-256-GCM (aes-256-gcm) and that runtime-specific
adapters (e.g., BunCryptoAdapter and WebCryptoAdapter, in addition to the
Node.js crypto adapter) implement that algorithm for Bun, Deno/Web, and Node.js
respectively; mention that the concrete provider varies by runtime and point to
those adapters as the implementations.

In `@examples/encrypted-workflow.js`:
- Around line 66-78: The manifest encryption fields used are incorrect: replace
references to manifest.encryption.iv and manifest.encryption.authTag with the
actual fields produced by NodeCryptoAdapter.#buildMeta
(manifest.encryption.nonce and manifest.encryption.tag), decode those base64
strings to Buffers to compute lengths and to produce the hex dump (e.g.,
Buffer.from(manifest.encryption.nonce, 'base64') and
Buffer.from(manifest.encryption.tag, 'base64')), keep algorithm fallback to
'AES-256-GCM' and preserve the error exit branch when
manifest.encryption.encrypted is falsy.

In `@examples/progress-tracking.js`:
- Around line 200-210: The summary is wrong because progress.store (and
progress.store.chunks/bytes) keeps accumulating across both store steps due to
the persistent "chunk:stored" listener; fix by either unregistering that
listener before Step 4 (call the emitter's off/removeListener for the same
handler used when subscribing) or by snapshotting the Step 1 totals immediately
after Step 1 completes (e.g., store storeChunksStep1 = progress.store.chunks and
storeBytesStep1 = progress.store.bytes) and then use those snapshot variables in
the summary/throughput calculation instead of progress.store.*; locate the
"chunk:stored" listener registration and the summary block that reads
progress.store to implement one of these fixes.

In `@GUIDE.md`:
- Around line 388-393: The example incorrectly imports writeFileSync from
'node:crypto'; change the import so randomBytes stays imported from
'node:crypto' and writeFileSync is imported from 'node:fs' (i.e., use
randomBytes and writeFileSync with correct module imports) so generating the
32-byte key with randomBytes(32) and writing it to './vacation.key' succeeds
without ImportError.

In `@ROADMAP.md`:
- Around line 135-136: The Markdown table rows for "v1.5.0" and "v1.6.0" have an
extra cell (the trailing ✅) causing a column count mismatch; fix by either
merging the checkmark into the existing "Theme" cell for those rows (so the
Theme column for v1.5.0 and v1.6.0 becomes e.g. "Observability ✅" and
"Documentation ✅") or by adding a new "Status" column to the table header and
moving each trailing ✅ into that new "Status" column so the header and all rows
have the same number of cells.

In `@src/infrastructure/adapters/WebCryptoAdapter.js`:
- Around line 11-23: The WebCryptoAdapter.randomBytes implementation returns a
Uint8Array but the CryptoPort.randomBytes contract expects a Buffer; update
WebCryptoAdapter.randomBytes to detect if Buffer exists on globalThis and, when
available, convert the Uint8Array into a Buffer (otherwise continue returning
the Uint8Array) so callers that rely on Buffer methods (e.g., .toString('hex'))
won't break; locate the randomBytes method inside class WebCryptoAdapter and
ensure behavior matches CryptoPort.randomBytes' documented return type while
preserving compatibility with environments that only accept Uint8Array.

In `@test/benchmark/cas.bench.js`:
- Around line 160-188: The decrypt benchmarks pass enc.buffer which is undefined
because CasService.encrypt() (and CryptoPort.encryptBuffer()) returns { buf,
meta }; update all benchmark calls to service.decrypt to pass enc.buf (not
enc.buffer) — specifically in the decrypt blocks for 'decrypt – 1KB', 'decrypt –
1MB' and 'decrypt – 10MB' replace usages of enc.buffer with enc.buf so decrypt
receives the actual encrypted bytes and meta remains unchanged.
🧹 Nitpick comments (8)
src/domain/value-objects/Chunk.js (1)

25-28: Consider using the return value of ChunkSchema.parse().

ChunkSchema.parse(data) returns the validated (and potentially transformed) data, but the result is discarded. If a Zod transform or default is ever added to ChunkSchema, Object.assign(this, data) would silently use the raw input instead of the parsed output.

♻️ Suggested improvement
-      ChunkSchema.parse(data);
-      Object.assign(this, data);
+      const validated = ChunkSchema.parse(data);
+      Object.assign(this, validated);
GUIDE.md (1)

347-364: Inconsistent import paths for internal modules across examples.

Line 359 uses a deep source path (@git-stunts/git-cas/src/domain/value-objects/Manifest.js), while line 853 uses a subpath export (@git-stunts/git-cas/service). Deep source paths are fragile and may break if the internal directory structure changes. Consider using consistent public API imports or subpath exports throughout the guide, or add a note that these are internal paths.

src/infrastructure/adapters/BunCryptoAdapter.js (2)

40-48: decryptBuffer validates key here but NodeCryptoAdapter.decryptBuffer does not.

BunCryptoAdapter.decryptBuffer calls #validateKey(key) (line 42), while NodeCryptoAdapter.decryptBuffer (see NodeCryptoAdapter.js lines 33–39) skips validation. Since CasService.decrypt doesn't call _validateKey before delegating, an invalid key passed to NodeCryptoAdapter will produce a raw Node crypto error rather than a clean CasError. Consider adding #validateKey to NodeCryptoAdapter.decryptBuffer as well for consistency.


50-83: createEncryptionStream has a streamFinalized guard — nice, but missing from NodeCryptoAdapter.

The STREAM_NOT_CONSUMED check (lines 72–77) is a useful safeguard. NodeCryptoAdapter.createEncryptionStream lacks this guard and will call cipher.getAuthTag() before cipher.final() if finalize() is invoked prematurely — resulting in a cryptic Node error. Consider porting this guard.

src/infrastructure/adapters/WebCryptoAdapter.js (1)

49-70: decryptBuffer does not validate the key — inconsistent with encryptBuffer.

encryptBuffer calls #validateKey (line 27), but decryptBuffer does not. An invalid key will produce a cryptic Web Crypto error instead of a clean CasError. This matches the same gap in NodeCryptoAdapter. Consider adding this.#validateKey(key) at the top of decryptBuffer for defensive consistency.

Proposed fix
   async decryptBuffer(buffer, key, meta) {
+    this.#validateKey(key);
     const nonce = this.#fromBase64(meta.nonce);
index.js (1)

58-68: Redundant initialization: this.#servicePromise = null in constructor duplicates the field declaration.

Line 65 sets this.#servicePromise = null, but the private field declaration at line 68 already initializes it to null before the constructor body runs. The assignment in the constructor is a no-op.

Proposed fix — remove redundant assignment
   constructor({ plumbing, chunkSize, codec, policy, crypto }) {
     this.plumbing = plumbing;
     this.chunkSizeConfig = chunkSize;
     this.codecConfig = codec;
     this.policyConfig = policy;
     this.cryptoConfig = crypto;
     this.service = null;
-    this.#servicePromise = null;
   }
src/domain/services/CasService.js (1)

145-155: decrypt does not validate the key before delegating to the crypto port.

encrypt (line 132) calls _validateKey, but decrypt does not. If called directly (outside of restore), an invalid key will produce a raw crypto error instead of CasError('INVALID_KEY_TYPE'). Consider adding _validateKey when meta?.encrypted is truthy, for symmetry with encrypt.

Proposed fix
   async decrypt({ buffer, key, meta }) {
     if (!meta?.encrypted) {
       return buffer;
     }
+    this._validateKey(key);
     try {
       return await this.crypto.decryptBuffer(buffer, key, meta);
examples/progress-tracking.js (1)

170-180: Hardcoded chunk size in progress estimation.

Line 175 uses the literal 128 * 1024 to estimate total chunks instead of cas.chunkSize, duplicating the value from line 30. If the chunk size on line 30 changes, this estimate silently becomes wrong.

♻️ Proposed fix
-    totalChunks = Math.ceil(fileSize / (128 * 1024));
+    totalChunks = Math.ceil(fileSize / cas.chunkSize);

- SECURITY.md: generalize crypto description for multi-runtime support
- encrypted-workflow.js: fix field names (nonce/tag, not iv/authTag)
- progress-tracking.js: snapshot Step 1 counters to avoid accumulation
- GUIDE.md: fix writeFileSync import (node:fs, not node:crypto)
- ROADMAP.md: add Status column header to fix table column mismatch
- WebCryptoAdapter: wrap randomBytes return in Buffer when available
- cas.bench.js: fix enc.buffer → enc.buf in all decrypt benchmarks
@flyingrobots
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@flyingrobots flyingrobots merged commit 0222feb into main Feb 7, 2026
6 checks passed
@flyingrobots flyingrobots deleted the cartographer branch February 7, 2026 03:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant